﻿// ------------------------------------------------------------------------------------
// File:		OperationTracker.cs
// Created:	01/03/2012 11:49 AM
// 
// Last modified:	tri @ 01/03/2012 1:57 PM
// ------------------------------------------------------------------------------------

namespace EntityFramework.OT
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Serialization;


    #region ObjectOperationTracker

    [DataContract(IsReference = true)]
    public class ObjectOperationTracker
    {
        #region Constants and Fields

        private NavigationAddDictionary _navigationAddDictionary;

        private NavigationCollectionAddDictionary _navigationCollectionAddDictionary;

        private NavigationCollectionDeleteDictionary _navigationCollectionDeleteDictionary;

        private NavigationRemoveDictionary _navigationRemoveDictionary;

        private bool _operationTrackingEnabled = true;

        private PropertyChangeDictionary _propertyChanges;

        private ObjectState _state = ObjectState.Added;

        #endregion

        #region Constructors and Destructors

        public ObjectOperationTracker()
        {
        }

        public ObjectOperationTracker(EntityInfo entityInfo)
        {
            EntityInfo = entityInfo;
        }

        #endregion

        #region Properties

        [DataMember]
        public EntityInfo EntityInfo { get; set; }

        [DataMember]
        public NavigationAddDictionary NavigationAdds
        {
            get { return _navigationAddDictionary ?? (_navigationAddDictionary = new NavigationAddDictionary()); }
        }

        [DataMember]
        public NavigationCollectionAddDictionary NavigationCollectionAdds
        {
            get
            {
                return _navigationCollectionAddDictionary ??
                        (_navigationCollectionAddDictionary = new NavigationCollectionAddDictionary());
            }
        }

        [DataMember]
        public NavigationCollectionDeleteDictionary NavigationCollectionDeletes
        {
            get
            {
                return _navigationCollectionDeleteDictionary ??
                        (_navigationCollectionDeleteDictionary = new NavigationCollectionDeleteDictionary());
            }
        }

        [DataMember]
        public NavigationRemoveDictionary NavigationDeletes
        {
            get { return _navigationRemoveDictionary ?? (_navigationRemoveDictionary = new NavigationRemoveDictionary()); }
        }

        public bool OperationTrackingEnabled
        {
            get { return _operationTrackingEnabled; }
            set { _operationTrackingEnabled = value; }
        }

        [DataMember]
        public PropertyChangeDictionary PropertyChanges
        {
            get { return _propertyChanges ?? (_propertyChanges = new PropertyChangeDictionary()); }
        }

        [DataMember]
        public ObjectState State
        {
            get { return _state; }
            set { _state = value; }
        }

        #endregion

        #region Public Methods

        public void AcceptChanges()
        {
            PropertyChanges.Clear();
            NavigationAdds.Clear();
            NavigationDeletes.Clear();
            NavigationCollectionAdds.Clear();
            NavigationCollectionDeletes.Clear();
            _operationTrackingEnabled = true;
            _state = ObjectState.Unchanged;
        }

        public void RecordCollectionAddOperation(string collectionName, ObjectOperationTracker operationTracker)
        {
            if (_operationTrackingEnabled)
            {
                // Add the operation back after deleting it, we should do nothing here then
                if (NavigationCollectionDeletes.ContainsKey(collectionName) &&
                    NavigationCollectionDeletes[collectionName].Contains(operationTracker))
                {
                    NavigationCollectionDeletes[collectionName].Remove(operationTracker);
                    if (NavigationCollectionDeletes[collectionName].Count == 0)
                    {
                        NavigationCollectionDeletes.Remove(collectionName);
                    }
                    return;
                }

                if (!NavigationCollectionAdds.ContainsKey(collectionName))
                {
                    NavigationCollectionAdds[collectionName] = new OperationTrackerList { operationTracker };
                }
                else
                {
                    NavigationCollectionAdds[collectionName].Add(operationTracker);
                }
            }
        }

        public void RecordCollectionDeleteOperation(string collectionName, ObjectOperationTracker operationTracker)
        {
            if (_operationTrackingEnabled)
            {
                // Delete the entity back after adding it, we should do nothing here then
                if (NavigationCollectionAdds.ContainsKey(collectionName) &&
                    NavigationCollectionAdds[collectionName].Contains(operationTracker))
                {
                    NavigationCollectionAdds[collectionName].Remove(operationTracker);
                    if (NavigationCollectionAdds[collectionName].Count == 0)
                    {
                        NavigationCollectionAdds.Remove(collectionName);
                    }
                    return;
                }

                if (!NavigationCollectionDeletes.ContainsKey(collectionName))
                {
                    NavigationCollectionDeletes[collectionName] = new OperationTrackerList { operationTracker };
                }
                else
                {
                    NavigationCollectionDeletes[collectionName].Add(operationTracker);
                }
            }
        }

        public void RecordNavigationPropertyOperation(
            string propertyName, IObjectWithOperationTracker previousValue, IObjectWithOperationTracker newValue)
        {
            if (_operationTrackingEnabled)
            {
                // Adding back a previously removed item. Remove and do nothing.
                if (NavigationDeletes.ContainsKey(propertyName) && NavigationDeletes[propertyName] == newValue)
                {
                    NavigationDeletes.Remove(propertyName);
                    return;
                }

                // If new value is not null, then record change.
                if (newValue != null)
                {
                    if (NavigationAdds.ContainsKey(propertyName))
                    {
                        NavigationAdds[propertyName] = newValue.OperationTracker;
                    }
                    else
                    {
                        NavigationAdds.Add(propertyName, newValue.OperationTracker);
                    }
                }
                else if (previousValue != null && previousValue.OperationTracker.State != ObjectState.Added)
                {
                    // If previous value is not in added state, record removal.
                    NavigationDeletes.Add(propertyName, previousValue.OperationTracker);
                }
            }
        }

        public void RecordPropertyOperation(string propertyName, object value)
        {
            if (_operationTrackingEnabled)
            {
                // Update the key to the entity info.
                if (EntityInfo.EntityKeys.ContainsKey(propertyName))
                {
                    EntityInfo.EntityKeys[propertyName] = value;
                }

                if (PropertyChanges.ContainsKey(propertyName))
                {
                    PropertyChanges[propertyName] = value;
                }
                else
                {
                    PropertyChanges.Add(propertyName, value);
                }
            }
        }

        #endregion
    }

    [DataContract(IsReference = true)]
    public class EntityInfo
    {
        #region Constants and Fields

        private EntityKeyDictionary _entityKeys;

        private Type _entityType;

        #endregion

        #region Constructors and Destructors

        public EntityInfo(string entityName, string entitySetName, params KeyValuePair<string, object>[] keys)
            : this()
        {
            EntityName = entityName;
            EntitySetName = entitySetName;
            if (keys != null)
            {
                foreach (var key in keys)
                {
                    EntityKeys.Add(key.Key, key.Value);
                }
            }
        }

        private EntityInfo()
        {
            EntityID = Guid.NewGuid();
        }

        #endregion

        #region Properties

        [DataMember]
        public Guid EntityID { get; set; }

        [DataMember]
        public EntityKeyDictionary EntityKeys
        {
            get { return _entityKeys ?? (_entityKeys = new EntityKeyDictionary()); }
        }

        [DataMember]
        public string EntityName { get; set; }

        [DataMember]
        public string EntitySetName { get; set; }

        public Type EntityType
        {
            get { return _entityType ?? (_entityType = Type.GetType(EntityName)); }
        }

        #endregion

        #region Operators

        /// <summary>
        ///     Compare two <see cref="EntityInfo" /> for <see cref="EntityInfo.EntityType" /> match.
        /// </summary>
        /// <param name="x"> </param>
        /// <param name="y"> </param>
        /// <returns> </returns>
        public static bool operator ==(EntityInfo x, EntityInfo y)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(x, y))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
            {
                return false;
            }

            return x.Equals(y);
        }

        public static bool operator !=(EntityInfo x, EntityInfo y)
        {
            return !(x == y);
        }

        #endregion

        #region Public Methods

        public bool Equals(EntityInfo other)
        {
            if (ReferenceEquals(null, other))
            {
                return false;
            }
            if (ReferenceEquals(this, other))
            {
                return true;
            }
            return other.EntityID.Equals(EntityID) && Equals(other.EntityName, EntityName) &&
                    Equals(other.EntitySetName, EntitySetName);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
            {
                return false;
            }
            if (ReferenceEquals(this, obj))
            {
                return true;
            }
            if (obj.GetType() != typeof(EntityInfo))
            {
                return false;
            }
            return Equals((EntityInfo)obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int result = EntityID.GetHashCode();
                result = (result * 397) ^ (EntityName != null ? EntityName.GetHashCode() : 0);
                result = (result * 397) ^ (EntitySetName != null ? EntitySetName.GetHashCode() : 0);
                return result;
            }
        }

        #endregion
    }

    public interface IObjectWithOperationTracker : IObjectWithChangeTracker
    {
        #region Properties

        ObjectOperationTracker OperationTracker { get; set; }

        #endregion
    }

    #endregion

    #region CollectionDataContract

    [CollectionDataContract(Name = "EntityKeyDictionary", ItemName = "EntityKey", KeyName = "Key", ValueName = "Value")]
    public class EntityKeyDictionary : Dictionary<string, object>
    {
    }

    [CollectionDataContract(Name = "PropertyChangeDictionary", ItemName = "Property", KeyName = "Name",
        ValueName = "Value")]
    public class PropertyChangeDictionary : Dictionary<string, object>
    {
    }

    [CollectionDataContract(Name = "NavigationAddDictionary", ItemName = "Navigation", KeyName = "PropertyName",
        ValueName = "OperationTracker")]
    public class NavigationAddDictionary : Dictionary<string, ObjectOperationTracker>
    {
    }

    [CollectionDataContract(Name = "NavigationRemoveDictionary", ItemName = "Navigation", KeyName = "PropertyName",
        ValueName = "OperationTracker")]
    public class NavigationRemoveDictionary : Dictionary<string, ObjectOperationTracker>
    {
    }

    [CollectionDataContract(Name = "NavigationCollectionAddDictionary", ItemName = "Navigation",
        KeyName = "CollectionName", ValueName = "OperationTrackers")]
    public class NavigationCollectionAddDictionary : Dictionary<string, OperationTrackerList>
    {
    }

    [CollectionDataContract(Name = "NavigationCollectionDeleteDictionary", ItemName = "Navigation",
        KeyName = "CollectionName", ValueName = "OperationTrackers")]
    public class NavigationCollectionDeleteDictionary : Dictionary<string, OperationTrackerList>
    {
    }

    [CollectionDataContract(ItemName = "OperationTracker")]
    public class OperationTrackerList : List<ObjectOperationTracker>
    {
        #region Indexers

        public ObjectOperationTracker this[EntityInfo info]
        {
            get { return this.First(i => i.EntityInfo == info); }
        }

        #endregion
    }

    #endregion

    #region Extensions

    public static class OperationTrackerExtensions
    {
        #region Public Methods

        public static IObjectWithOperationTracker ApplyChanges(
            this ObjectOperationTracker tracker, IObjectWithOperationTracker item = null, bool acceptOperations = true)
        {
            if (tracker == null)
            {
                throw new ArgumentNullException("tracker");
            }
            if (item != null)
            {
                if (item.OperationTracker.EntityInfo != tracker.EntityInfo)
                {
                    throw new InvalidOperationException(string.Format(
                        "This operation is not for the item of type {0}.", item.OperationTracker.EntityInfo.EntityType));
                }
            }

            var helper = new OperationHelper(tracker, item);
            foreach (var operation in helper.Operations.Where(o => o.State != ObjectState.Unchanged))
            {
                var entity = helper[operation];
                if (entity == null)
                {
                    throw new InvalidOperationException(string.Format("Could not find or create entity with id {0}.",
                        operation.EntityInfo.EntityID));
                }
                // Apply property operations.
                foreach (var pair in operation.PropertyChanges)
                {
                    entity.SetValue(pair.Key, pair.Value);
                }
            }

            foreach (var operation in helper.Operations)
            {
                var entity = helper[operation];
                if (entity == null)
                {
                    throw new InvalidOperationException(string.Format("Could not find or create entity with id {0}.",
                        operation.EntityInfo.EntityID));
                }

                if (operation.State == ObjectState.Deleted)
                {
                    entity.MarkAsDeleted();
                    continue;
                }

                // Apply navigational property operations
                foreach (var pair in operation.NavigationAdds)
                {
                    entity.SetValue(pair.Key, helper[pair.Value]);
                }

                // Apply property add operations
                foreach (var pair in operation.NavigationCollectionAdds)
                {
                    var collection = (IList)entity.GetValue(pair.Key);
                    foreach (var relative in pair.Value)
                    {
                        collection.Add(helper[relative]);
                    }
                }

                // Apply property delete operations
                foreach (var op in operation.NavigationCollectionDeletes)
                {
                    var collection = (IList)entity.GetValue(op.Key);
                    foreach (var relative in op.Value)
                    {
                        collection.Remove(helper[relative]);
                    }
                }

                if (acceptOperations)
                {
                    entity.AcceptChanges();
                }
            }
            return item;
        }

        #endregion

        private class OperationHelper
        {
            #region Constants and Fields

            private readonly IDictionary<ObjectOperationTracker, IObjectWithOperationTracker> _operations;

            #endregion

            #region Constructors and Destructors

            public OperationHelper(ObjectOperationTracker tracker, IObjectWithOperationTracker entity)
            {
                _operations = new Dictionary<ObjectOperationTracker, IObjectWithOperationTracker>();
                AddAllOperations(tracker, entity);
            }

            #endregion

            #region Properties

            public IEnumerable<ObjectOperationTracker> Operations
            {
                get { return _operations.Keys; }
            }

            #endregion

            #region Indexers

            public IObjectWithOperationTracker this[ObjectOperationTracker key]
            {
                get { return _operations[key]; }
            }

            #endregion

            #region Methods

            private void AddAllOperations(ObjectOperationTracker tracker, IObjectWithOperationTracker entity)
            {
                // If entity is null, then create it.
                if (entity == null)
                {
                    entity = CreateEntity(tracker);
                }

                // Add itself to the operations list.
                AddOperation(tracker, entity);

                foreach (var operation in tracker.NavigationAdds.Where(o => !Contains(o.Value)))
                {
                    var subEntity = (IObjectWithOperationTracker)entity.GetValue(operation.Key) ??
                                    CreateEntity(operation.Value);
                    AddAllOperations(operation.Value, subEntity);
                }

                foreach (var operation in tracker.NavigationCollectionAdds)
                {
                    var collection =
                        ((IEnumerable)entity.GetValue(operation.Key)).Cast<IObjectWithOperationTracker>().ToList();
                    foreach (var subOperation in operation.Value.Where(o => !Contains(o)))
                    {
                        var subEntity = GetEntity(collection, subOperation);
                        AddAllOperations(subOperation, subEntity);
                    }
                }

                foreach (var operation in tracker.NavigationCollectionDeletes)
                {
                    var collection =
                        ((IEnumerable)entity.GetValue(operation.Key)).Cast<IObjectWithOperationTracker>().ToList();
                    foreach (var subOperation in operation.Value.Where(o => !Contains(o)))
                    {
                        var subEntity = GetEntity(collection, subOperation);
                        AddAllOperations(subOperation, subEntity);
                    }
                }
            }

            private void AddOperation(ObjectOperationTracker tracker, IObjectWithOperationTracker entity)
            {
                if (!_operations.ContainsKey(tracker))
                {
                    _operations.Add(tracker, entity);
                }
            }

            private bool Contains(ObjectOperationTracker tracker)
            {
                return _operations.ContainsKey(tracker);
            }

            private IObjectWithOperationTracker CreateEntity(ObjectOperationTracker tracker)
            {
                var entity = (IObjectWithOperationTracker)Activator.CreateInstance(tracker.EntityInfo.EntityType);
                entity.OperationTracker.EntityInfo = tracker.EntityInfo;
                tracker.State = ObjectState.Modified;
                return entity;
            }

            private IObjectWithOperationTracker GetEntity(
                IEnumerable<IObjectWithOperationTracker> entities, ObjectOperationTracker operation)
            {
                return entities.FirstOrDefault(i => i.OperationTracker.EntityInfo == operation.EntityInfo) ??
                        CreateEntity(operation);
            }

            #endregion
        }
    }

    public static class ObjectWithOperationTrackerExtensions
    {
        #region Public Methods
        public static T MarkAsDeleted<T>(this T trackingItem) where T : class, IObjectWithOperationTracker
        {
            if (trackingItem == null)
            {
                throw new ArgumentNullException("trackingItem");
            }

            trackingItem.ChangeTracker.ChangeTrackingEnabled = true;
            trackingItem.OperationTracker.OperationTrackingEnabled = true;
            trackingItem.ChangeTracker.State = ObjectState.Deleted;
            trackingItem.OperationTracker.State = ObjectState.Deleted; ;
            return trackingItem;
        }

        public static T MarkAsAdded<T>(this T trackingItem) where T : class, IObjectWithOperationTracker
        {
            if (trackingItem == null)
            {
                throw new ArgumentNullException("trackingItem");
            }

            trackingItem.ChangeTracker.ChangeTrackingEnabled = true;
            trackingItem.OperationTracker.OperationTrackingEnabled = true;
            trackingItem.ChangeTracker.State = ObjectState.Added;
            trackingItem.OperationTracker.State = ObjectState.Added; ;
            return trackingItem;
        }

        public static T MarkAsModified<T>(this T trackingItem) where T : class, IObjectWithOperationTracker
        {
            if (trackingItem == null)
            {
                throw new ArgumentNullException("trackingItem");
            }

            trackingItem.ChangeTracker.ChangeTrackingEnabled = true;
            trackingItem.OperationTracker.OperationTrackingEnabled = true;
            trackingItem.ChangeTracker.State = ObjectState.Modified;
            trackingItem.OperationTracker.State = ObjectState.Modified;;
            return trackingItem;
        }

        public static T MarkAsUnchanged<T>(this T trackingItem) where T : class, IObjectWithOperationTracker
        {
            if (trackingItem == null)
            {
                throw new ArgumentNullException("trackingItem");
            }

            trackingItem.ChangeTracker.ChangeTrackingEnabled = true;
            trackingItem.OperationTracker.OperationTrackingEnabled = true;
            trackingItem.ChangeTracker.State = ObjectState.Unchanged;
            trackingItem.OperationTracker.State = ObjectState.Unchanged; ;
            return trackingItem;
        }

        public static void AcceptChanges(this IObjectWithOperationTracker trackingItem)
        {
            if (trackingItem == null)
            {
                throw new ArgumentNullException("trackingItem");
            }

            trackingItem.OperationTracker.AcceptChanges();
            trackingItem.ChangeTracker.AcceptChanges();
        }

        public static void StartTracking(this IObjectWithOperationTracker trackingItem)
        {
            if (trackingItem == null)
            {
                throw new ArgumentNullException("trackingItem");
            }

            trackingItem.ChangeTracker.ChangeTrackingEnabled = true;
            trackingItem.OperationTracker.OperationTrackingEnabled = true;
        }

        public static void StopTracking(this IObjectWithOperationTracker trackingItem)
        {
            if (trackingItem == null)
            {
                throw new ArgumentNullException("trackingItem");
            }

            trackingItem.ChangeTracker.ChangeTrackingEnabled = false;
            trackingItem.OperationTracker.OperationTrackingEnabled = false;
        }

        public static object GetValue(this IObjectWithOperationTracker trackingItem, string propertyName)
        {
            var property = trackingItem.OperationTracker.EntityInfo.EntityType.GetProperty(propertyName);
            if (property == null)
            {
                throw new MemberAccessException(String.Format("Cannot access entity property {0}", propertyName));
            }
            return property.GetValue(trackingItem, null);
        }

        public static void SetValue(this IObjectWithOperationTracker trackingItem, string propertyName, object value)
        {
            var property = trackingItem.OperationTracker.EntityInfo.EntityType.GetProperty(propertyName);
            if (property == null)
            {
                throw new MemberAccessException(String.Format("Cannot access entity property {0}", propertyName));
            }
            property.SetValue(trackingItem, value, null);
        }

        #endregion
    }

    #endregion

}